/*
 * Copyright (C)  Tony Green, LitePal Framework Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.litepal.lib.crud;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;

import com.litepal.lib.crud.model.AssociationsInfo;
import com.litepal.lib.exceptions.DataSupportException;
import com.litepal.lib.util.BaseUtility;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This is a component under DataSupport. It deals with the updating stuff as
 * primary task. Either updating specifying data with id or updating multiple
 * lines with conditions can be done here.
 *
 * @author Tony Green
 * @since 1.1
 */
class UpdateHandler extends DataHandler
{

	/**
	 * Initialize {@link com.litepal.lib.crud.DataHandler#mDatabase} for operating database. Do not
	 * allow to create instance of UpdateHandler out of CRUD package.
	 *
	 * @param db The instance of SQLiteDatabase.
	 */
	UpdateHandler(SQLiteDatabase db)
	{
		mDatabase = db;
	}

	/**
	 * The open interface for other classes in CRUD package to update. Using
	 * baseObj to decide which table to update, and id to decide a specific row.
	 * The value that need to update is stored in baseObj.
	 *
	 * @param baseObj Which table to update by model instance.
	 * @param id      Which record to update.
	 * @return The number of rows affected.
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws IllegalArgumentException
	 * @throws SecurityException
	 */
	int onUpdate(DataSupport baseObj, long id) throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException
	{
		List<Field> supportedFields = getSupportedFields(baseObj.getClassName());
		ContentValues values = new ContentValues();
		putFieldsValue(baseObj, supportedFields, values);
		putFieldsToDefaultValue(baseObj, values);
		if (values.size() > 0)
		{
			return mDatabase.update(baseObj.getTableName(), values, "id = " + id, null);
		}
		return 0;
	}

	/**
	 * The open interface for other classes in CRUD package to update. Using
	 * modelClass to decide which table to update, and id to decide a specific
	 * row. The value that need to update is stored in ContentValues.
	 *
	 * @param modelClass Which table to update by class.
	 * @param id         Which record to update.
	 * @param values     A map from column names to new column values. null is a valid
	 *                   value that will be translated to NULL.
	 * @return The number of rows affected.
	 */
	int onUpdate(Class<?> modelClass, long id, ContentValues values)
	{
		if (values.size() > 0)
		{
			return mDatabase.update(getTableName(modelClass), values, "id = " + id, null);
		}
		return 0;
	}

	/**
	 * The open interface for other classes in CRUD package to update multiple
	 * rows. Using baseObj to decide which table to update, and conditions
	 * representing the WHERE part of an SQL statement. The value that need to
	 * update is stored in baseObj.
	 *
	 * @param baseObj    Which table to update by model instance.
	 * @param conditions A string array representing the WHERE part of an SQL
	 *                   statement.
	 * @return The number of rows affected.
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws IllegalArgumentException
	 * @throws SecurityException
	 */
	int onUpdateAll(DataSupport baseObj, String... conditions) throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException
	{
		List<Field> supportedFields = getSupportedFields(baseObj.getClassName());
		ContentValues values = new ContentValues();
		putFieldsValue(baseObj, supportedFields, values);
		putFieldsToDefaultValue(baseObj, values);
		return doUpdateAllAction(baseObj.getTableName(), values, conditions);
	}

	/**
	 * The open interface for other classes in CRUD package to update multiple
	 * rows. Using tableName to decide which table to update, and conditions
	 * representing the WHERE part of an SQL statement. The value that need to
	 * update is stored in ContentValues.
	 *
	 * @param tableName  Which table to update.
	 * @param conditions A string array representing the WHERE part of an SQL
	 *                   statement.
	 * @param values     A map from column names to new column values. null is a valid
	 *                   value that will be translated to NULL.
	 * @return The number of rows affected.
	 */
	int onUpdateAll(String tableName, ContentValues values, String... conditions)
	{
		return doUpdateAllAction(tableName, values, conditions);
	}

	/**
	 * Do the action for updating multiple rows. It will check the validity of
	 * conditions, then update rows in database. If the format of conditions is
	 * invalid, throw DataSupportException.
	 *
	 * @param tableName  Which table to delete from.
	 * @param conditions A string array representing the WHERE part of an SQL
	 *                   statement.
	 * @param values     A map from column names to new column values. null is a valid
	 *                   value that will be translated to NULL.
	 * @return The number of rows affected.
	 */
	private int doUpdateAllAction(String tableName, ContentValues values, String... conditions)
	{
		BaseUtility.checkConditionsCorrect(conditions);
		if (values.size() > 0)
		{
			return mDatabase.update(tableName, values, getWhereClause(conditions), getWhereArgs(conditions));
		}
		return 0;
	}

	/**
	 * Iterate all the fields that need to set to default value. If the field is
	 * id, ignore it. Or put the default value of field into ContentValues.
	 *
	 * @param baseObj Which table to update by model instance.
	 * @param values  To store data of current model for persisting or updating.
	 */
	private void putFieldsToDefaultValue(DataSupport baseObj, ContentValues values)
	{
		String fieldName = null;
		try
		{
			DataSupport emptyModel = getEmptyModel(baseObj);
			Class<?> emptyModelClass = emptyModel.getClass();
			for (String name : baseObj.getFieldsToSetToDefault())
			{
				if (!isIdColumn(name))
				{
					fieldName = name;
					Field field = emptyModelClass.getDeclaredField(fieldName);
					putContentValuesForUpdate(emptyModel, field, values);
				}
			}
		}
		catch (NoSuchFieldException e)
		{
			throw new DataSupportException(DataSupportException.noSuchFieldExceptioin(baseObj.getClassName(), fieldName), e);
		}
		catch (Exception e)
		{
			throw new DataSupportException(e.getMessage(), e);
		}
	}

	/**
	 * Unused currently.
	 */
	@SuppressWarnings("unused")
	private int doUpdateAssociations(DataSupport baseObj, long id, ContentValues values)
	{
		int rowsAffected = 0;
		analyzeAssociations(baseObj);
		updateSelfTableForeignKey(baseObj, values);
		rowsAffected += updateAssociatedTableForeignKey(baseObj, id);
		return rowsAffected;
	}

	/**
	 * Analyze the associations of baseObj and store the result in it. The
	 * associations will be used when deleting referenced data of baseObj.
	 * Unused currently.
	 *
	 * @param baseObj The record to update.
	 */
	private void analyzeAssociations(DataSupport baseObj)
	{
		try
		{
			Collection<AssociationsInfo> associationInfos = getAssociationInfo(baseObj.getClassName());
			analyzeAssociatedModels(baseObj, associationInfos);
		}
		catch (Exception e)
		{
			throw new DataSupportException(e.getMessage(), e);
		}
	}

	/**
	 * Unused currently.
	 */
	private void updateSelfTableForeignKey(DataSupport baseObj, ContentValues values)
	{
		Map<String, Long> associatedModelMap = baseObj.getAssociatedModelsMapWithoutFK();
		for (String associatedTable : associatedModelMap.keySet())
		{
			String fkName = getForeignKeyColumnName(associatedTable);
			values.put(fkName, associatedModelMap.get(associatedTable));
		}
	}

	/**
	 * Unused currently.
	 */
	private int updateAssociatedTableForeignKey(DataSupport baseObj, long id)
	{
		Map<String, Set<Long>> associatedModelMap = baseObj.getAssociatedModelsMapWithFK();
		ContentValues values = new ContentValues();
		for (String associatedTable : associatedModelMap.keySet())
		{
			values.clear();
			String fkName = getForeignKeyColumnName(baseObj.getTableName());
			values.put(fkName, id);
			Set<Long> ids = associatedModelMap.get(associatedTable);
			if (ids != null && !ids.isEmpty())
			{
				return mDatabase.update(associatedTable, values, getWhereOfIdsWithOr(ids), null);
			}
		}
		return 0;
	}

}